/*
 License
 
 Copyright © 2012 Kevin Kimmich
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights to 
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifdef ENABLE_SSL

#include "list.h"
#include "stream.h"
#include "kuato_types.h"
#include "kuato_http_operations.h"
#include "kuato_server.h"
#include "messaging.h"

static SSL_CTX* client_app_ctx = NULL;

#if 0
static void apps_ssl_info_callback (SSL * s, int where, int ret)
{
	char *str;
	int w = where & ~SSL_ST_MASK;
	if (w & SSL_ST_CONNECT)
		str="SSL_connect";
	else if (w & SSL_ST_ACCEPT)
		str="SSL_accept";
	else
		str="undefined";
	if (where & SSL_CB_LOOP) {
		printf("%s: %s\n",str,SSL_state_string_long(s));
	} else if (where & SSL_CB_ALERT) {
		str = (where & SSL_CB_READ) ? "read" : "write";
		printf("SSL3 alert %s:%s:%s\n",str,
				SSL_alert_type_string_long(ret),
						SSL_alert_desc_string_long(ret));
	} else if (where & SSL_CB_EXIT) {
		if (ret == 0) {
			printf("%s: failed in %s\n",str,SSL_state_string_long(s));
		} else if (ret < 0) {
			printf("%s: error in %s\n",str,SSL_state_string_long(s));
		}
	}
}
#endif
				    
connection_context* create_ssl_client_context(struct sockaddr_in addr)
{	
	connection_context* ctx = malloc(sizeof(connection_context));
	if(ctx == NULL)
		return NULL;
	
	memset(ctx,0,sizeof(connection_context));
	
	ctx->type = CLIENT;
	
	ctx->fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	if(ctx->fd < 0)
		goto fail;
	
	struct linger lng = {0,0};
	if(setsockopt(ctx->fd,SOL_SOCKET,SO_LINGER,(void*)&lng,sizeof(struct linger)))
		goto fail;
	
	ctx->addr = addr;
	if(connect(ctx->fd,(struct sockaddr*)&ctx->addr,sizeof(struct sockaddr_in)))
	{
		if(errno != EINPROGRESS)
			goto fail;
	}		
	
	size_t optlen = sizeof(size_t);
	if(getsockopt(ctx->fd,SOL_SOCKET,SO_RCVBUF,&ctx->read_buffer_size,(socklen_t*)&optlen))
		goto fail;
	
	ctx->ssl = SSL_new(client_app_ctx);    /* create new SSL connection state */
	int res = SSL_set_fd(ctx->ssl, ctx->fd);   /* attach the socket descriptor */

	if(res != 1)
		printf("SSL_set_fd failed %d\n",res);
	
	int failed=0;
	for(;;)
	{
		res=SSL_connect(ctx->ssl);           /* start the handshaking */
		if(res != 1)
		{
			int err= SSL_get_error(ctx->ssl,res);
			printf("SSL_connect failed %d|%d\n",res,err);
			if(!failed && (res==SSL_ERROR_WANT_READ || res==SSL_ERROR_WANT_WRITE))
			{
				failed=1;
				continue;
			}
			goto fail;
		}
		else
			break;
	}

	stream_init(&ctx->read_stream,UNBOUNDED);
	stream_init(&ctx->write_stream,UNBOUNDED);
	
	return ctx;
	
fail:
	if(ctx->ssl)
		SSL_free(ctx->ssl);

	if(ctx->fd >= 0)
		close(ctx->fd);

	free(ctx);
	return NULL;
}

static void* ssl_listener_loop(int argc, void** arg)
{
	connection_context* c = arg[0];
	int fd;
	
	while(1)
	{
		socklen_t len = sizeof(struct sockaddr_in);
		struct sockaddr_in address = {0};
		fd = accept(c->fd, (struct sockaddr*)&address, (socklen_t*)&len);
		if(fd < 0)
			goto fail;
		
		
		connection_context* ctx = create_ssl_server_context(c->http_srv->sslctx,address,fd);
		if(ctx==NULL)
			continue;

		ctx->http_srv = c->http_srv;
		ctx->srvctx = c;
		
		//the context gets cleaned up by the handle_connection
		//function called in the new thread
		void* argv[1];
		argv[0] = ctx;
		ctx->thread_id = core_thread_create_attached(handle_http_connection,1,argv);
	}

fail:
	//there was an error
	if(c)
		destroy_context(c);
	if(fd > 0)
		close(fd);
	
	return NULL;
}


//create a context that listens on address with a max_conn backlog
connection_context* create_ssl_listener_context(struct sockaddr_in addr, int max_conn,http_server_object* h)
{
	connection_context* ctx = malloc(sizeof(connection_context));
	if(ctx == NULL)
		return NULL;
	
	memset(ctx,0,sizeof(connection_context));
	
	ctx->type = SSL_LISTENER;
	
	ctx->fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(ctx->fd < 0)
		goto fail;
	
	int reuse = 1;
	if(setsockopt(ctx->fd,SOL_SOCKET,SO_REUSEADDR,(void*)&reuse,sizeof(reuse)))
		goto fail;
	
	struct linger lng = {0,0};
	if(setsockopt(ctx->fd,SOL_SOCKET,SO_LINGER,(void*)&lng,sizeof(struct linger)))
		goto fail;
	
	memcpy(&ctx->addr,&addr,sizeof(struct sockaddr_in));
	if(bind(ctx->fd,(struct sockaddr*)&ctx->addr,sizeof(struct sockaddr_in)))
		goto fail;
	
	if(listen(ctx->fd,max_conn))
		goto fail;
	
	memcpy(&ctx->addr,&addr,sizeof(struct sockaddr_in));
	ctx->http_srv=h;
	void* argv[1];
	argv[0]=ctx;
	ctx->thread_id = core_thread_create_attached(ssl_listener_loop,1,argv);
	return ctx;
	
fail:
	if(ctx->fd >= 0)
		close(ctx->fd);
	free(ctx);
	return NULL;
}

connection_context* create_ssl_server_context(SSL_CTX* sslctx, struct sockaddr_in addr, int fd)
{
	connection_context* ctx = malloc(sizeof(connection_context));
	if(ctx == NULL)
		goto fail;
	
	memset(ctx,0,sizeof(connection_context));
	
	list_init(&ctx->methods);
	
	ctx->type = SERVER;
	ctx->fd = fd;
	ctx->addr = addr;
	
	struct linger lng = {0,0};
	if(setsockopt(ctx->fd,SOL_SOCKET,SO_LINGER,(void*)&lng,sizeof(struct linger)))
		goto fail;
	
	struct timeval tv;
	tv.tv_sec=5;
	tv.tv_usec=0;
	if(setsockopt(ctx->fd,SOL_SOCKET,SO_RCVTIMEO,(void*)&tv,sizeof(struct timeval)))
		goto fail;
	
	size_t optlen = sizeof(size_t);
	if(getsockopt(ctx->fd,SOL_SOCKET,SO_RCVBUF,&ctx->read_buffer_size,(socklen_t*)&optlen))
		goto fail;

	ctx->ssl = SSL_new(sslctx);
	SSL_set_fd(ctx->ssl, ctx->fd);    /* set connection to SSL state */
	
	for(;;)
	{
		int res=SSL_accept(ctx->ssl);           /* start the handshaking */
		if(res!=1)
		{
			//int err=SSL_get_error(ctx->ssl,res);
			//printf("  ERR %d\n",err);
		}
		else
			break;
		
		//printf("SSL_accept = %d %p fd=%d\n",res,ctx->ssl,ctx->fd);
		if(res!=1)
			goto fail;
	}
	
	stream_init(&ctx->read_stream,UNBOUNDED);
	stream_init(&ctx->write_stream,UNBOUNDED);	
	return ctx;
	
fail:
	if(ctx->fd >= 0)
		close(ctx->fd);
	if(ctx->ssl)
	{
		//printf("SSL_free %p\n",ctx->ssl);
		SSL_free(ctx->ssl);
	}
	free(ctx);
	return NULL;
}

/////////////////////

//static int verify_depth=1;

/*static int verify_callback (int ok, X509_STORE_CTX * ctx)
{
	int verify_error=0;
	char buf[256];
	X509 * err_cert = X509_STORE_CTX_get_current_cert(ctx);
	int err = X509_STORE_CTX_get_error(ctx);
	int depth = X509_STORE_CTX_get_error_depth(ctx);
	X509_NAME_oneline(X509_get_subject_name(err_cert), buf, 256);
	
	printf("verify %d\n",ok);

	printf( "depth = %d %s\n",depth,buf);
	if (!ok) {
		printf( "verify error: num=%d:%s\n",err ,
				X509_verify_cert_error_string(err));
		if (verify_depth >= depth) {
			ok=1;
			verify_error=X509_V_OK;
		} else {
			ok=0;
			verify_error=X509_V_ERR_CERT_CHAIN_TOO_LONG;
		}
	}
	switch (ctx->error) {

		case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
			X509_NAME_oneline(X509_get_issuer_name(ctx->current_cert), buf, 256);
			printf( "issuer= %s\n",buf);
			break;

		case X509_V_ERR_CERT_NOT_YET_VALID:
		case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
			printf( "notBefore=");
//	ASN1_TIME_print(bio_err,X509_get_notBefore(ctx->current_cert));
			printf( "\n");
			break;

		case X509_V_ERR_CERT_HAS_EXPIRED:
		case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
			printf( "notAfter=");
//	ASN1_TIME_print(bio_err,X509_get_notAfter(ctx->current_cert));
			printf( "\n");
			break;
	}
	printf( "verify return: %d\n",ok);
	return ok;
}*/




pthread_mutex_t* lock_cs = NULL;
long* lock_count = NULL;
static unsigned long id_function(void)
{
	return ((unsigned long) pthread_self());
}

static void locking_function(int mode, int type, const char *file, int line)
{
	if (mode & CRYPTO_LOCK) {
		pthread_mutex_lock(&lock_cs[type]);
		lock_count[type]++;
	} else {
		pthread_mutex_unlock(&lock_cs[type]);
	}
}


static SSL_CTX* __messaging_create_client_ssl_context(void)
{
	char rnd_filename[4096];
	RAND_file_name(rnd_filename, sizeof(rnd_filename));
	RAND_load_file(rnd_filename, -1);
	
	SSL_METHOD* client_method = NULL;
	SSLeay_add_ssl_algorithms();
	SSL_load_error_strings();
	client_method = (SSL_METHOD*)SSLv3_client_method();
	
	return SSL_CTX_new(client_method);
}

SSL_CTX* messaging_create_server_ssl_context(const char* cert_file, const char* key_file)
{
	SSL_CTX* ctx = NULL;	
	
	SSL_METHOD * server_method = NULL;
		
	server_method = (SSL_METHOD*)SSLv23_method();
		
		/* set up the application context */
	ctx = SSL_CTX_new(server_method);
	
	if(ctx==NULL)
		return NULL;
	
	
	/* See the SSL states in our own callback */
	//SSL_CTX_set_info_callback(server_app_ctx, apps_ssl_info_callback);
	
	/* Set the certificate verification callback */
	//SSL_CTX_set_verify(app_ctx, SSL_VERIFY_PEER, verify_callback);
	
	/* Set client cert stuff if it exists */
	if (cert_file != NULL) {
		if (SSL_CTX_use_certificate_file(ctx,cert_file,
										 SSL_FILETYPE_PEM) <= 0) {
			printf("HTSSL... unable to get certificate from '%s'\n" , cert_file);
			return NULL;
		}
		if (key_file == NULL) key_file=cert_file;
		if (SSL_CTX_use_PrivateKey_file(ctx,key_file,
										SSL_FILETYPE_PEM) <= 0) {
			printf("HTSSL... unable to get private key from '%s'\n",key_file);
			return NULL;
		}
		if (!SSL_CTX_check_private_key(ctx)) {
			printf("HTSSL... Private key does not match the certificate public key\n");
			return NULL;
		}
	}
	return ctx;	
}

static int ssl_initialized = 0;
int messaging_ssl_init(void)
{
	if(ssl_initialized)
		return 0;
	
	char rnd_filename[4096];
	RAND_file_name(rnd_filename, sizeof(rnd_filename));
	RAND_load_file(rnd_filename, -1);

	lock_count = malloc(sizeof(long)*CRYPTO_num_locks());
	lock_cs = malloc(sizeof(pthread_mutex_t)*CRYPTO_num_locks());
	int i;
	for(i=0;i<CRYPTO_num_locks();i++)
	{
		lock_count[i]=0;
		pthread_mutex_init(&lock_cs[i],NULL);
	}
	
	CRYPTO_set_id_callback(id_function);
	CRYPTO_set_locking_callback(locking_function);
	
	SSL_library_init();
	SSL_load_error_strings();
	
	client_app_ctx = __messaging_create_client_ssl_context();
		
	return 0;
}

#endif //ENABLE_SSL